package com.simplechain.link;

import com.simplechain.constant.TransactionConstant;
import com.simplechain.constant.TransactionConstant.Type;
import com.simplechain.contract.ContractCall;
import com.simplechain.util.Utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import link.luyu.protocol.algorithm.ecdsa.secp256k1.SignatureData;

import link.luyu.protocol.common.STATUS;
import link.luyu.protocol.link.Connection;
import link.luyu.protocol.link.Driver;
import link.luyu.protocol.network.*;
import link.luyu.toolkit.abi.ContractABI;
import link.luyu.toolkit.abi.FunctionABI;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.datatypes.Function;
import org.web3j.protocol.core.methods.response.TransactionReceipt;


public class SipcDriver implements Driver {
    private Connection connection;

    private static final ObjectMapper objectMapper = new ObjectMapper();


    public SipcDriver(Connection connection, Map<String, Object> properties) throws Exception {
        this.connection = connection;

    }

    public void start() throws RuntimeException {

    }

    public void stop() throws RuntimeException {

    }

    private static byte[] join(byte[]... params) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] res = null;
        try {
            for (int i = 0; i < params.length; i++) {
                baos.write(params[i]);
            }
            res = baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return res;
    }

    @Override
    public void sendTransaction(Account account, Transaction transaction, ReceiptCallback callback) {

        String contract = Utils.getResourceName(transaction.getPath());
//        String contract = "0x7d96bc41d27ca16b9212e55dad8a28443e696c64";
        connection.asyncSend(contract, Type.GET_ABI, account.getPubKey(), (errorCode, msg, responseData) -> {
                    if (errorCode != STATUS.OK) {
                        callback.onResponse(errorCode, msg, null);
                    } else {
                        String abi = new String(responseData);

                        connection.asyncSend("", Type.GET_BLOCK_NUMBER, null, (errorCode1, msg1, responseData1) -> {
                                    if (errorCode1 != STATUS.OK) {
                                        callback.onResponse(errorCode1, msg1, null);
                                    } else {
                                        long blockNumber = Utils.bytesToLong(responseData1);
                                        String data = FunctionEncoder.encode(
                                                Utils.convertFunction(
                                                        abi, transaction.getMethod(), transaction.getArgs()));

                                        TransactionDataVo transactionDataVo = new TransactionDataVo();
                                        transactionDataVo.data = data;
                                        transactionDataVo.contract = contract;
                                        transactionDataVo.address = Utils.publicKeyToEthAddress(account.getPubKey());


                                        try {
                                            byte[] transactionData = objectMapper.writeValueAsBytes(transactionDataVo);

                                            connection.asyncSend(
                                                    "1",
                                                    Type.SEND_TRANSACTION,
                                                    transactionData,
                                                    (errorCode2, message2, data2) -> {
                                                        RawTransactionVo rawTransactionVo = null;
                                                        try {
                                                            rawTransactionVo = objectMapper.readValue(data2, RawTransactionVo.class);
                                                        } catch (IOException e) {
                                                            e.printStackTrace();
                                                            callback.onResponse(STATUS.INTERNAL_ERROR, "create RawTransaction object error", null);
                                                            return;
                                                        }

                                                        if (rawTransactionVo == null) {
                                                            callback.onResponse(STATUS.INTERNAL_ERROR, "create RawTransaction object error", null);
                                                            return;
                                                        }

                                                        byte[] encodedTransaction = rawTransactionVo.encodedTransaction;
                                                        Integer rawTransactionHashcode = rawTransactionVo.rawTransactionHashcode;
                                                        //签名
                                                        CompletableFuture<byte[]> future = new CompletableFuture();
                                                        account.sign(
                                                                encodedTransaction,
                                                                new Account.SignCallback() {
                                                                    @Override
                                                                    public void onResponse(int status, String message, byte[] signBytes) {
                                                                        if (status != STATUS.OK) {
                                                                            future.complete(null);
                                                                        } else {
                                                                            future.complete(signBytes);
                                                                        }
                                                                    }
                                                                }
                                                        );


                                                        byte[] luyuSignBytes = new byte[0];
                                                        byte[] signedMessage = new byte[0];


                                                        try {
                                                            luyuSignBytes = future.get(30, TimeUnit.SECONDS);
                                                            //account签名
                                                            SignatureData luyuSignData = SignatureData.parseFrom(luyuSignBytes);

                                                        } catch (Exception e) {
                                                            e.printStackTrace();
                                                            callback.onResponse(
                                                                    STATUS.INTERNAL_ERROR,
                                                                    "account sign failed,error:" + e.getMessage(),
                                                                    null);
                                                            return;
                                                        }

                                                        RawTransactionVo newRawTransactionVo = new RawTransactionVo();
                                                        newRawTransactionVo.rawTransactionHashcode=rawTransactionHashcode;
                                                        newRawTransactionVo.luyuSignBytes=luyuSignBytes;
                                                        byte[] rawTransactionVoBytes = new byte[0];
                                                        try {
                                                            rawTransactionVoBytes = objectMapper.writeValueAsBytes(newRawTransactionVo);
                                                        } catch (JsonProcessingException e) {
                                                            e.printStackTrace();
                                                        }

                                                        connection.asyncSend(
                                                                "2",
                                                                Type.SEND_TRANSACTION,
                                                                rawTransactionVoBytes,
                                                                (errorCode4, message4, responseData2) -> {


                                                                    byte[] transactionHash = null;
                                                                    if (responseData2 != null) {
                                                                        transactionHash = responseData2;
                                                                    }

                                                                    //等待出块
                                                                    AtomicBoolean flag = new AtomicBoolean(true);
                                                                    for (int n = 0; flag.get(); n++) {
                                                                        connection.asyncSend("", Type.GET_BLOCK_NUMBER, null, (errorCode3, msg3, responseData3) -> {
                                                                            long currentNumber = 0;
                                                                            if (errorCode3 != STATUS.OK) {
                                                                                callback.onResponse(errorCode3, msg3, null);
                                                                            } else {
                                                                                currentNumber = Utils.bytesToLong(responseData3);
                                                                            }

                                                                            if (currentNumber > blockNumber) {
                                                                                flag.set(false);
                                                                            }

                                                                        });

                                                                        //超时
                                                                        if (n > 20) {
                                                                            flag.set(false);
                                                                        }

                                                                        try {
                                                                            Thread.sleep(500);
                                                                        } catch (InterruptedException e) {
                                                                            e.printStackTrace();
                                                                        }

                                                                    }





                                                                    //出块后查询交易信息
                                                                    connection.asyncSend(
                                                                            null,
                                                                            TransactionConstant.Type.GET_TRANSACTION_RECEIPT,
                                                                            transactionHash,
                                                                            (errorCode3, message, responseData3) -> {
                                                                                if (errorCode3 != STATUS.OK) {
                                                                                    callback.onResponse(errorCode3, message, null);
                                                                                    return;
                                                                                }
                                                                                if (responseData3.length < 7) {
                                                                                    callback.onResponse(errorCode3, "Not fund this transaction.", null);
                                                                                }
                                                                                try {
                                                                                    objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
                                                                                    TransactionReceipt transactionReceipt =
                                                                                            objectMapper.readValue(responseData3, TransactionReceipt.class);
                                                                                    Receipt receipt = new Receipt();
                                                                                    receipt.setResult(new String[]{new String(responseData3)});
                                                                                    receipt.setBlockNumber(
                                                                                            Long.parseLong(transactionReceipt.getBlockNumberRaw()));
                                                                                    receipt.setCode(0); // SUCCESS
                                                                                    receipt.setMessage("Success");
                                                                                    receipt.setTransactionBytes(responseData3);
                                                                                    receipt.setTransactionHash(transactionReceipt.getTransactionHash());
                                                                                    callback.onResponse(STATUS.OK, "Success", receipt);
                                                                                } catch (IOException e) {
                                                                                    callback.onResponse(
                                                                                            STATUS.INTERNAL_ERROR,
                                                                                            "deserialize failed,error:" + e.getMessage(),
                                                                                            null);
                                                                                }
                                                                            }
                                                                    );
                                                                }
                                                        );
                                                    }
                                            );
                                        } catch (Exception e) {
                                            callback.onResponse(
                                                    STATUS.INTERNAL_ERROR, "serialize failed,error:" + e.getMessage(), null);
                                            e.printStackTrace();
                                            throw new RuntimeException("get transaction receipt ,error:" + e.getMessage());
                                        }

                                    }
                                }
                        );
                    }
                }
        );
    }

    @Override
    public void call(Account account, CallRequest callRequest, CallResponseCallback callback) {
        String contract = Utils.getResourceName(callRequest.getPath());

        connection.asyncSend(contract, Type.GET_ABI, account.getPubKey(), (errorCode, msg, responseData) -> {
            if (errorCode != STATUS.OK) {
                callback.onResponse(errorCode, msg, null);
            } else {
                String abi = new String(responseData);

                ContractABI ctAbi = new ContractABI(abi);
                FunctionABI funAbi;
                try {
                    funAbi = ctAbi.getFunctions(callRequest.getMethod()).get(0);
                } catch (IndexOutOfBoundsException ioe) {
                    callback.onResponse(
                            STATUS.INTERNAL_ERROR,
                            "no method found",
                            null);
                    return;
                }
                Function function = Utils.convertFunction(abi, callRequest.getMethod(), callRequest.getArgs());
                ContractCall call = new ContractCall(contract, FunctionEncoder.encode(function));
                byte[] pubKey = account.getPubKey();
                String sender = Utils.publicKeyToEthAddress(pubKey);
                call.setSender(sender);
                try {
                    byte[] data = objectMapper.writeValueAsBytes(call);
                    connection.asyncSend(
                            callRequest.getPath(),
                            Type.CALL_TRANSACTION,
                            data,
                            (errorCode1, message1, responseData1) -> {
                                CallResponse callResponse = new CallResponse();
                                if (responseData1 != null) {
                                    String resp = new String(responseData1);
                                    if (!resp.equals("0x")) {
                                        callResponse.setResult(funAbi.decodeOutput(resp));
                                    }
                                }
                                callResponse.setCode(0); // original receipt status
                                callResponse.setMessage("Success");
                                callResponse.setMethod(callRequest.getMethod());
                                callResponse.setArgs(callRequest.getArgs());
                                callResponse.setPath(callRequest.getPath());
                                callback.onResponse(STATUS.OK, "Success", callResponse);
                            });
                } catch (JsonProcessingException e) {
                    callback.onResponse(
                            STATUS.INTERNAL_ERROR, "serialize failed,error:" + e.getMessage(), null);
                    throw new RuntimeException("serialize failed");
                }
            }
        });

    }

    @Override
    public void getTransactionReceipt(String txHash, ReceiptCallback callback) {
        connection.asyncSend(
                null,
                TransactionConstant.Type.GET_TRANSACTION_RECEIPT,
                txHash.getBytes(),
                (errorCode, message, responseData) -> {
                    try {
                        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
                        TransactionReceipt transactionReceipt =
                                objectMapper.readValue(responseData, TransactionReceipt.class);
                        Receipt receipt = new Receipt();
                        receipt.setResult(new String[]{new String(responseData)});
                        receipt.setBlockNumber(
                                Long.parseLong(transactionReceipt.getBlockNumberRaw()));
                        receipt.setCode(0); // SUCCESS
                        receipt.setMessage("Success");
                        receipt.setTransactionBytes(responseData);
                        receipt.setTransactionHash(transactionReceipt.getTransactionHash());
                        callback.onResponse(STATUS.OK, "Success", receipt);
                    } catch (IOException e) {
                        callback.onResponse(
                                STATUS.INTERNAL_ERROR,
                                "deserialize failed,error:" + e.getMessage(),
                                null);
                    }
                });
    }

    @Override
    public void getBlockByHash(String blockHash, BlockCallback callback) {
        connection.asyncSend(blockHash, Type.GET_BLOCK_BY_HASH, null, (errorCode, message, responseData) -> {
            if (errorCode != STATUS.OK) {
                callback.onResponse(errorCode, message, null);
            } else {
                Block luyuBlock = null;
                try {
                    luyuBlock = objectMapper.readValue(responseData,Block.class);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                callback.onResponse(STATUS.OK, "Success", luyuBlock);
            }
        });
    }

    @Override
    public void getBlockByNumber(long blockNumber, BlockCallback callback) {
        connection.asyncSend("", Type.GET_BLOCK_BY_NUMBER, Utils.longToBytes(blockNumber), (errorCode, message, responseData) -> {
            if (errorCode != STATUS.OK) {
                callback.onResponse(errorCode, message, null);
            } else {
                Block luyuBlock = null;
                try {
                    luyuBlock = objectMapper.readValue(responseData,Block.class);
                } catch (IOException e) {
                    e.printStackTrace();
                }
//                com.simplechain.util.Utils.saveFile("","getBlockByNumber:"+luyuBlock,true);
                callback.onResponse(STATUS.OK, "Success", luyuBlock);
            }
        });
    }

    @Override
    public long getBlockNumber() {
        CompletableFuture<byte[]> getBlockNumberFuture = new CompletableFuture<>();
        connection.asyncSend("", Type.GET_BLOCK_NUMBER, null, new Connection.Callback() {
            @Override
            public void onResponse(int errorCode, String message, byte[] responseData) {
                if (errorCode != STATUS.OK) {
                    getBlockNumberFuture.complete(null);
                } else {
                    getBlockNumberFuture.complete(responseData);
                }
            }
        });

        byte[] bsBlockNumber;
        try {
            bsBlockNumber = getBlockNumberFuture.get(20, TimeUnit.SECONDS);
            if (bsBlockNumber == null) {
                return -1;
            }
        } catch (Exception e) {
            return -1;
        }

        long blockNumber = Utils.bytesToLong(bsBlockNumber);
        return blockNumber;
    }

    @Override
    public String getType() {
        return "Sipc";
    }

    @Override
    public String getSignatureType() {
        return "ECDSA_SECP256K1_WITH_SHA256";
    }

    @Override
    public void listResources(ResourcesCallback callback) {
        Collection<Resource> resources = new HashSet<>();

        connection.asyncSend("",
                Type.GET_ContractAddressAll,
                null,
                (errorCode1, message1, responseData1) -> {
                    if (errorCode1 != STATUS.OK) {
                        callback.onResponse(errorCode1, message1, null);
                    } else {
                        try {
                            ArrayList<String> resourcePathList = objectMapper.readValue(responseData1, ArrayList.class);

                            for (String resourcePath : resourcePathList) {
                                Resource resource = new Resource();
                                resource.setPath(resourcePath);
                                resource.setType("Sipc");
                                resource.setMethods(new String[]{});
                                resource.setProperties(null);
                                resources.add(resource);
                            }

                            callback.onResponse(
                                    0,
                                    "Success",
                                    resources.toArray(new Resource[resources.size()]));
                        } catch (IOException e) {
                            e.printStackTrace();
                        }

                    }
                });

    }

    @Override
    public void registerEvents(Events events) {
    }

}
